Go语言逆向初探
本文为看雪论坛优秀文章
Go的二进制逆向在互联网上有一篇很全面的文章《Go二进制文件逆向分析从基础到进阶》(https://www.anquanke.com/post/id/214940),建议有兴趣的朋友读一下。珠玉在前为什么还要写这篇文章呢?这是因为Go最新版本1.16有所变化,以前的解析办法已经不适用,这里把自己在逆向Go外挂过程中的一些经验沉淀下来。
1
Go语言特点介绍
Go是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。Go的语法接近C语言,但对于变量的声明有所不同,与C++相比,Go并不包括如枚举、异常处理、继承、泛型、断言、虚函数等功能,但增加了 切片(Slice) 型、并发、管道、垃圾回收功能、接口等特性的语言级支持[1]。
在这里简单介绍一下在逆向Go程序过程中需要用到的一些特性知识。
1、反射
反射是逆向最好的朋友,如果一门语言具备反射那么其编译出的可执行文件本身就约等于符号文件,这不是靠strip命令或者加壳压缩就能删除的。
最好的证明就是UE4引擎编译出来的游戏程序,UE4采用C++开发,该语言实际不支持反射,但UE4实现了一套反射机制从而泄露了函数名、类等信息,这也是外挂作者最喜欢的东西。反射的实现通常依赖于所谓的Metadata即数据的数据,不同的语言可能有不同的称呼。
作为一篇逆向文章,本文不会介绍Go语言的反射语法或者作用,相反这里讲解的是怎么通过Metadata来读懂二进制文件。
在Go语言程序中存在一个叫pcHeader 的结构,也就是所谓的Meatadata。pcHeader 结构参考symtab.go源代码如下图所示,值得注意的是Go 1.16之前的MagicNumber是0xffffffb,之后是0xfffffffa,并且nfunc和nfiles实际占用8个字节。
pcHeader的地址可以搜索FA FF FF FF 00 00 01 08或者FA FF FF FF 00 00 01 04获得,其中x64程序是0x08,32位程序是0x04。
pcHeader + pclnOffset即指向pclntab也就是Go用来描述函数信息的地方,该结构由funcAddress + funcMetaAddress两部分组成。
pcHeader + pclnOffset + funcMetaAddress指向Go的_func结构,具体见symtab.go文件的type pcHeader struct。通过_func中的entry也就是funcAddress和nameOff就可以把函数地址和函数名结合起来。
2、Stack
Go 语言用的是 continue stack 栈管理机制 [2],并且 Go 语言函数中 callee 的栈空间由 caller 来维护,callee 的参数、返回值都由 caller 在栈中预留空间。详见 The Go low-level calling convention on x86-64[3]。Go支持goroutine也就是协程,每个goroutine都有自己的栈,其初始栈空间很小并在使用过程中自动增长。
这种机制使得Go编译出来的函数在起始处判断当前栈是否够用,如果不够用就分配足够大的新空间并将旧栈数据拷贝到新栈中。该特点表现到二进制中如下,可以利用该特点定位到Go语言函数,当然有部分函数也不会判断栈空间是否够用。
3、字符串
Go 二进制文件中的 string 数据不是传统的以 0x00 结尾的 C语言系字符串,而是采用StartAddress + Size的模式,例如下面程序传入的是Hello和Test的起始地址和长度。
2
Go逆向突破口
所谓的逆向突破口就是把程序中的所有函数标识并命名出来,知道了函数名那么距离解析出具体功能就只差一步之遥。下面是我编写IDA脚本用于半自动重命名函数,具体效果见下图:
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
'''
SimpleGoParser.py:
IDA Plugin for Golang Executable file parsing.
'''
import idautils, idc, idaapi
import sys
import string
def getstring(addr):
out = ""
while True:
value = idc.get_wide_byte(addr)
if value != 0:
out += chr(value)
else:
break
addr += 1
return out
STRIP_CHARS = [ '(', ')', '[', ']', '{', '}', ' ', '"' ]
REPLACE_CHARS = ['.', '*', '-', ',', ';', ':', '/', '\xb7' ]
def cleanfuncname(funcName):
for c in STRIP_CHARS:
funcName = funcName.replace(c, '')
for c in REPLACE_CHARS:
funcName = funcName.replace(c, '_')
return funcName
class Pclntab():
# symtab.go file
# type pcHeader struct {
# magic uint32 // 0xFFFFFFFA
# pad1, pad2 uint8 // 0,0
# minLC uint8 // min instruction size
# ptrSize uint8 // size of a ptr in bytes
# nfunc int // number of functions in the module
# nfiles uint // number of entries in the file tab.
# funcnameOffset uintptr // offset to the funcnametab variable from pcHeader
# cuOffset uintptr // offset to the cutab variable from pcHeader
# filetabOffset uintptr // offset to the filetab variable from pcHeader
# pctabOffset uintptr // offset to the pctab varible from pcHeader
# pclnOffset uintptr // offset to the pclntab variable from pcHeader
# }
def __init__(self):
self.MAGIC = 0xFFFFFFFA
self.offset = 0
self.baseAddress = 0
self.minLC = 0
self.ptrSize = 0
self.functionCounts = 0
self.fileCounts = 0
self.functionNameOffset = 0
self.cuOffset = 0
self.filetabOffset = 0
self.pctabOffset = 0
self.functabOffset = 0
def parse(self, baseAddress):
print("Pclntab Go")
magic = idc.get_wide_dword(baseAddress)
if magic != self.MAGIC:
print("Pclntab address invalid");
return False
self.baseAddress = baseAddress
self.offset = 4 + 2
self.minLC = idc.get_wide_byte(baseAddress + self.offset)
self.offset = self.offset + 1
self.ptrSize = idc.get_wide_byte(baseAddress + self.offset)
self.offset = self.offset + 1
self.functionCounts = idc.get_wide_dword(baseAddress + self.offset)
self.offset = self.offset + 8 # 静态文件中实际占8字节
self.fileCounts = idc.get_wide_dword(baseAddress + self.offset)
self.offset = self.offset + 8 # 静态文件中实际占8字节
self.functionNameOffset = baseAddress + idc.get_qword(baseAddress + self.offset)
self.offset = self.offset + 8
self.cuOffset = baseAddress + idc.get_qword(baseAddress + self.offset)
self.offset = self.offset + 8
self.filetabOffset = baseAddress + idc.get_qword(baseAddress + self.offset)
self.offset = self.offset + 8
self.pctabOffset = baseAddress + idc.get_qword(baseAddress + self.offset)
self.offset = self.offset + 8
self.functabOffset = baseAddress + idc.get_qword(baseAddress + self.offset)
print("Pclntab Base Info")
print("Min Instuction Size %d Pointer Size %d Function Counts %d File Counts %d" % (self.minLC, self.ptrSize, self.functionCounts, self.fileCounts))
print("Function Name Offset 0x%x CuOffset 0x%x File Table Offset 0x%x Pctab Offset 0x%x Function Table Offset 0x%x" % (self.functionNameOffset, self.cuOffset, self.filetabOffset, self.pctabOffset, self. functabOffset))
return True
def setfuncname(self):
index = 0
while index < self.functionCounts:
funcAddr = idc.get_qword(self.functabOffset + 2 * index * self.ptrSize)
funcMetaAddr = self.functabOffset + idc.get_qword(self.functabOffset + 2 * index * self.ptrSize + self.ptrSize)
if funcAddr != idc.get_qword(funcMetaAddr):
print("Traverse Function Failed")
else:
funcNameAddr = self.functionNameOffset + idc.get_wide_dword(funcMetaAddr + self.ptrSize)
funcName = getstring(funcNameAddr)
funcName = cleanfuncname(funcName)
print("Function Address 0x%x Name %s" % (funcAddr, funcName))
if idc.get_wide_dword(funcAddr) == 0x0c8b4865:
idc.create_insn(funcAddr)
idc.add_func(funcAddr)
idc.set_name(funcAddr, funcName, SN_CHECK)
index = index + 1
def main():
pc = Pclntab()
if pc.parse(0xE90E60) == True:
pc.setfuncname()
if __name__ == '__main__':
main()
未完待续
1、当前脚本需要手动寻找pclntab所在地址,可以优化自动寻找;
2、Go二进制中的数据结构解析;3、很多Go函数IDA不能很好识别,如下所示以字节码(65 48 8B 0C 25 28 00 00 00 mov rcx, gs:28h)开始的都是函数,即使调用idc.add_func()定义为函数IDA也不能全部转换,不知道道友们否有更好的解决办法?
引用
[1] https://zh.wikipedia.org/wiki/Go
[3] https://dr-knz.net/go-calling-convention-x86-64.html
看雪ID:半个大西瓜
https://bbs.pediy.com/user-home-771611.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!